// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu

#include "coin2/exchange/di/constant.h"

#include <map>
#include <string>

#include <doctest.h>

#include "coin2/exchange/base/feed/ws/feed_subscription_spec.h"
#include "coin2/exchange/base/market/market_exchange_api.h"
#include "coin2/exchange/base/symbology/symbology_spec.h"
#include "coin2/exchange/base/symbology/util.h"
#include "coin2/flow/symbol_group.h"

namespace coin2::exchange::di {

namespace {

auto load_product_infos(const std::experimental::filesystem::path& dir) {
  std::map<MarketExchangeApi, coin2::exchange::base::symbology::ProductInfoConstant> map;
  // FIXME : product_info.json files are not yet organized
  for (const auto& e : std::experimental::filesystem::directory_iterator(dir)) {
    const auto& path = e.path();
    auto mea_str = path.stem().string();
    if (std::count(mea_str.begin(), mea_str.end(), '.') == 2) {
      auto mea = MarketExchangeApi::FromStringNoThrow(mea_str);
      if (!mea.has_value()) {
        continue;
      }
      map.emplace(mea.value(), path);
    }
  }
  return map;
}

void load_product_infos(
    const std::experimental::filesystem::path& dir,
    const MarketExchangeApi& loaded_mea,
    std::map<MarketExchangeApi, coin2::exchange::base::symbology::ProductInfoConstant>* product_infos) {
  // FIXME : product_info.json files are not yet organized
  for (const auto& e : std::experimental::filesystem::directory_iterator(dir)) {
    const auto& path = e.path();
    auto mea_str = path.stem().string();
    if (std::count(mea_str.begin(), mea_str.end(), '.') == 2) {
      auto mea = MarketExchangeApi::FromStringNoThrow(mea_str);
      if (!mea.has_value()) {
        continue;
      }
      if (mea.value() == loaded_mea) {
        product_infos->emplace(mea.value(), path);
        return;
      }
    }
  }
}

}  // namespace

class IConstantInjection {
 public:
  virtual ~IConstantInjection() = default;
  virtual const SymbologySpec& GetSymbologySpec(const MarketExchangeApi& mea) const = 0;
  virtual const std::unordered_map<MarketExchangeApi, FeedSubscriptionSpec>& GetFeedSubscriptionSpecs() const = 0;
  virtual const FeedSubscriptionSpec& GetFeedSubscriptionSpec(const MarketExchangeApi& mea) const = 0;
  virtual const coin2::flow::SymbolGroupConfig& GetSymbolGroups() const = 0;
  virtual const coin2::exchange::base::symbology::ProductInfoConstant& GetProductInfo(const MarketExchangeApi& mea) const = 0;
  virtual const ArbitrationConfigMap& GetArbitrationConfigs() const =0;
};

class ConstantInjectionPreloadImpl : public IConstantInjection {
 public:
  explicit ConstantInjectionPreloadImpl(const ConstantInjectionBuilder& builder)
      : builder_(builder),
        feed_subscription_specs_(FeedSubscriptionSpec::FromToml(
            builder_.feed_subscribers_toml.value_or("data/coin2/feed/subscribers.toml"))),
        symbology_specs_(
            builder_.symbology_specs_toml.value_or("data/coin2/symbology/translation.toml")),
        symbol_groups_(
            builder_.feed_symbol_groups_toml.value_or("data/coin2/feed/symbol_groups.toml")),
        product_infos_(
            load_product_infos(builder_.product_info_dir.value_or("data/coin2/product_info"))),
        arbitration_configs_(
          symbol_groups_,
          builder_.arbitration_config_toml.value_or("data/coin2/feed/arbitration.toml")) {}

  virtual const SymbologySpec& GetSymbologySpec(const MarketExchangeApi& mea) const override {
    return symbology_specs_.at(mea);
  }

  virtual const std::unordered_map<MarketExchangeApi, FeedSubscriptionSpec>& GetFeedSubscriptionSpecs()
      const override {
    return feed_subscription_specs_;
  }
  virtual const FeedSubscriptionSpec& GetFeedSubscriptionSpec(const MarketExchangeApi& mea) const override{
    // Special handling for bitflyer spot
    if (mea.exchange == coin::proto::ExchangeType::Bitflyer) {
      return feed_subscription_specs_.at(
          MarketExchangeApi{
              coin::proto::MarketType::Futures,
              coin::proto::ExchangeType::Bitflyer,
              "v1"});
    }
    if (feed_subscription_specs_.find(mea) == feed_subscription_specs_.end()) {
      throw std::invalid_argument(
          "feed subscription info is not found for mea=<" + mea.String() + ">");
    }
    return feed_subscription_specs_.at(mea);
  }
  virtual const coin2::flow::SymbolGroupConfig& GetSymbolGroups() const override { return symbol_groups_; }
  virtual const coin2::exchange::base::symbology::ProductInfoConstant& GetProductInfo(
      const MarketExchangeApi& mea) const override {
    if (product_infos_.find(mea) == product_infos_.end()) {
      throw std::invalid_argument("no ProductInfo for mea=<" + mea.String() + ">");
    }
    return product_infos_.at(mea);
  }
  virtual const ArbitrationConfigMap& GetArbitrationConfigs() const override { return arbitration_configs_; }

 private:
  const ConstantInjectionBuilder builder_;
  const std::unordered_map<MarketExchangeApi, FeedSubscriptionSpec> feed_subscription_specs_;
  const SymbologySpecMap symbology_specs_;
  const coin2::flow::SymbolGroupConfig symbol_groups_;
  const std::map<MarketExchangeApi, coin2::exchange::base::symbology::ProductInfoConstant>
      product_infos_;
  const ArbitrationConfigMap arbitration_configs_;
};

class ConstantInjectionLazyLoadImpl : public IConstantInjection {
 public:
  explicit ConstantInjectionLazyLoadImpl(const ConstantInjectionBuilder& builder)
      : builder_(builder) {}

  virtual const SymbologySpec& GetSymbologySpec(const MarketExchangeApi& mea) const override {
    std::scoped_lock _(load_mutex_);
    if (!symbology_specs_) {
      symbology_specs_.reset(new SymbologySpecMap(
          builder_.symbology_specs_toml.value_or("data/coin2/symbology/translation.toml")));
    }
    return symbology_specs_->at(mea);
  }

  virtual const std::unordered_map<MarketExchangeApi, FeedSubscriptionSpec>& GetFeedSubscriptionSpecs()
      const override {
    std::scoped_lock _(load_mutex_);
    if (feed_subscription_specs_.empty()) {
      feed_subscription_specs_ = FeedSubscriptionSpec::FromToml(
          builder_.feed_subscribers_toml.value_or("data/coin2/feed/subscribers.toml"));
    }
    return feed_subscription_specs_;
  }
  virtual const FeedSubscriptionSpec& GetFeedSubscriptionSpec(const MarketExchangeApi& mea) const override {
    std::scoped_lock _(load_mutex_);
    if (feed_subscription_specs_.empty()) {
      feed_subscription_specs_ = FeedSubscriptionSpec::FromToml(
          builder_.feed_subscribers_toml.value_or("data/coin2/feed/subscribers.toml"));
    }
    // Special handling for bitflyer spot
    if (mea.exchange == coin::proto::ExchangeType::Bitflyer) {
      return feed_subscription_specs_.at(
          MarketExchangeApi{
              coin::proto::MarketType::Futures,
              coin::proto::ExchangeType::Bitflyer,
              "v1"});
    }
    if (feed_subscription_specs_.find(mea) == feed_subscription_specs_.end()) {
      throw std::invalid_argument(
          "feed subscription info is not found for mea=<" + mea.String() + ">");
    }
    return feed_subscription_specs_.at(mea);
  }
  virtual const coin2::flow::SymbolGroupConfig& GetSymbolGroups() const override {
    std::scoped_lock _(load_mutex_);
    if (!symbol_groups_) {
      symbol_groups_.reset(new coin2::flow::SymbolGroupConfig(
          builder_.feed_symbol_groups_toml.value_or("data/coin2/feed/symbol_groups.toml")));
    }
    return *symbol_groups_;
  }
  virtual const coin2::exchange::base::symbology::ProductInfoConstant& GetProductInfo(
      const MarketExchangeApi& mea) const override {
    std::scoped_lock _(load_mutex_);
    if (product_infos_.find(mea) == product_infos_.end()) {
      load_product_infos(builder_.product_info_dir.value_or("data/coin2/product_info"),
          mea, &product_infos_);
    }
    if (product_infos_.find(mea) == product_infos_.end()) {
      throw std::invalid_argument("no ProductInfo for mea=<" + mea.String() + ">");
    }
    return product_infos_.at(mea);
  }
  virtual const ArbitrationConfigMap& GetArbitrationConfigs() const override {
    std::scoped_lock _(load_mutex_);
    if (!arbitration_configs_) {
      if (!symbol_groups_) {
        symbol_groups_.reset(new coin2::flow::SymbolGroupConfig(
            builder_.feed_symbol_groups_toml.value_or("data/coin2/feed/symbol_groups.toml")));
      }
      arbitration_configs_.reset(new ArbitrationConfigMap(
          *symbol_groups_,
          builder_.arbitration_config_toml.value_or("data/coin2/feed/arbitration.toml")));
    }
    return *arbitration_configs_;
  }

 private:
  const ConstantInjectionBuilder builder_;
  mutable std::unordered_map<MarketExchangeApi, FeedSubscriptionSpec> feed_subscription_specs_;
  mutable std::unique_ptr<SymbologySpecMap> symbology_specs_;
  mutable std::unique_ptr<coin2::flow::SymbolGroupConfig> symbol_groups_;
  mutable std::map<MarketExchangeApi, coin2::exchange::base::symbology::ProductInfoConstant>
      product_infos_;
  mutable std::unique_ptr<ArbitrationConfigMap> arbitration_configs_;

  mutable std::mutex load_mutex_;
};

ConstantInjection::ConstantInjection(
    const ConstantInjectionBuilder& builder,
    bool lazy_load) {
  if (lazy_load) {
    impl_.reset(new ConstantInjectionLazyLoadImpl(builder));
  } else {
    impl_.reset(new ConstantInjectionPreloadImpl(builder));
  }
}

const SymbologySpec& ConstantInjection::GetSymbologySpec(
    const MarketExchangeApi& mea) const {
  return impl_->GetSymbologySpec(mea);
}

ConstantInjection::~ConstantInjection() = default;

const std::unordered_map<MarketExchangeApi, FeedSubscriptionSpec>&
ConstantInjection::GetFeedSubscriptionSpecs() const {
  return impl_->GetFeedSubscriptionSpecs();
}
const FeedSubscriptionSpec& ConstantInjection::GetFeedSubscriptionSpec(
    const MarketExchangeApi& mea) const {
  return impl_->GetFeedSubscriptionSpec(mea);
}
const coin2::flow::SymbolGroupConfig& ConstantInjection::GetSymbolGroups() const {
  return impl_->GetSymbolGroups();
}
const coin2::exchange::base::symbology::ProductInfoConstant&
ConstantInjection::GetProductInfo(const MarketExchangeApi& mea) const {
  return impl_->GetProductInfo(mea);
}

const ArbitrationConfigMap& ConstantInjection::GetArbitrationConfigs() const {
  return impl_->GetArbitrationConfigs();
}

}  // namespace coin2::exchange::di

DOCTEST_TEST_CASE("ConstantInjeciton") {
  coin2::exchange::di::ConstantInjectionBuilder builder;
  coin2::exchange::di::ConstantInjection ci(builder);
  DOCTEST_SUBCASE("constants - feed subscriber") {
    auto&& okex = ci.GetFeedSubscriptionSpec(
        MarketExchangeApi::FromString("Futures.Okex.v3"));
    (void)okex;
    DOCTEST_CHECK(okex.host == "real.okex.com");
    DOCTEST_CHECK(okex.handle_ping_msg == false);
    DOCTEST_CHECK(okex.channels.at("trade") == "futures/trade:{}");
    DOCTEST_CHECK(okex.recipes.at("realtime")[1] == "depth_realtime");
  }

  DOCTEST_SUBCASE("constants - symbol groups") {
    const auto& groups = ci.GetSymbolGroups();
    (void)groups;
    DOCTEST_CHECK(
        groups.GetGroupForRelativeNorm(
            MarketExchangeApi::FromString("Spot.Gopax.v1"), "COMP-BTC") == 'c');
    auto symbols = groups.GetRelativeNormSymbols(
        MarketExchangeApi::FromString("Spot.Bitfinex.v2"), 'e');
    (void)symbols;
    DOCTEST_CHECK(symbols.size() == 3);
    DOCTEST_CHECK(symbols[0] == "OMG-BTC");
  }
}
